/*
 * Copyright (c) 2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.libermundi.theorcs.core.dao.hibernate;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.List;

import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceContext;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.ArrayUtils;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Property;
import org.hibernate.criterion.Restrictions;
import org.libermundi.theorcs.core.dao.AbsctractGenericDao;
import org.libermundi.theorcs.core.model.base.Activable;
import org.libermundi.theorcs.core.model.base.Defaultable;
import org.libermundi.theorcs.core.model.base.Identifiable;
import org.libermundi.theorcs.core.model.base.Inheritable;
import org.libermundi.theorcs.core.model.base.Undeletable;
import org.libermundi.theorcs.core.util.ClazzUtils;
import org.libermundi.theorcs.core.util.PaginatedList;
import org.libermundi.theorcs.core.util.PaginatedListImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Basic Hibernate implementation of generic DAO.
 *
 * @param <T> entity type, it must implements at least <code>Identifiable</code>
 * @param <I> entity's primary key, it must be serializable
 * @see Identifiable
 */

public class HibernateGenericDao<T extends Identifiable<I>, I extends Serializable> extends AbsctractGenericDao<T, I> implements HibernateDao<T, I> {
	private static final Logger logger = LoggerFactory.getLogger(HibernateGenericDao.class);
	
	@PersistenceContext
    protected EntityManager _entityManager;

    /**
     * Default constructor. Use for extend this class.
     */
    @SuppressWarnings(value = "unchecked")
    public HibernateGenericDao() {
        _clazz = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        _mappedClazz = _clazz;
        checkGenericClass();
        if(logger.isDebugEnabled()) {
        	logger.debug("Instantiate Hibernate Manager for Entity : " + _clazz);
        }
    }

    /**
     * Constructor with given {@link Identifiable} implementation. Use for creating DAO without extending this class.
     *
     * @param clazz class with will be accessed by DAO methods
     */
    public HibernateGenericDao(Class<T> clazz) {
        _clazz = clazz;
        _mappedClazz = _clazz;
        checkGenericClass();
        if(logger.isDebugEnabled()) {
        	logger.debug("Instantiate Manager for Entity : " + _clazz);
        }
    }

    public HibernateGenericDao(EntityManager entityManager) {
    	this();
    	_entityManager = entityManager;
    }

    public HibernateGenericDao(Class<T> clazz, EntityManager entityManager) {
    	this(clazz);
    	_entityManager = entityManager;
    }

    /*
     * (non-Javadoc)
     * @see org.libermundi.theorcs.core.dao.GenericDao#get(java.io.Serializable)
     */
    @Override
	public final T get(I id) {
    	return get(id,true);
    }     	


    @Override
    public T get(I id, boolean handleUndeletable) {
    	if (id == null) {
    		return null;
    	}
    	Criterion criterion = Restrictions.eq(Identifiable.PROP_ID, id);
    	T result = findUniqueByCriteria(createSimpleCriteria().add(criterion));
    	
    	if (handleUndeletable) {
        	if(result instanceof Undeletable && ((Undeletable) result).isDeleted()){
        		return null;
        	}
        	return result;
        }
    	return result;
    }

    @SuppressWarnings("unchecked")
    @Override
    public final List<T> get(I... ids) {
        Criterion criterion = Restrictions.in(Identifiable.PROP_ID, ids);
        return findByCriteria(createSimpleCriteria().add(criterion));
    }

    @SuppressWarnings("unchecked")
    @Override
    public final List<T> get(boolean deletedHandle, I... ids) {
        Criteria criteria = createSimpleCriteria();

        Criterion criterion = Restrictions.in(Identifiable.PROP_ID, ids);
        criteria.add(criterion);
        if (deletedHandle && _deletable) {
            Criterion notDeleted = Restrictions.eq(Undeletable.PROP_DELETED, Boolean.FALSE);
            criteria.add(notDeleted);
        }
        return findByCriteria(criteria);
    }

    @Override
    public final List<T> get(Inheritable<T> parent) {
        if (parent == null) {
            Criterion criterion = Restrictions.isNull(Inheritable.PROP_PARENT);
            return findByCriteria(createSimpleCriteria().add(criterion));
        }
        Criterion criterion = Restrictions.eq(Inheritable.PROP_PARENT, parent);
        return findByCriteria(createSimpleCriteria().add(criterion));
    }


    @Override
    public final List<T> getAll(boolean handleUndeletable) {
        Criteria criteria = createSimpleCriteria();
        if (_deletable && handleUndeletable) {
            Criterion notDeleted = Restrictions.eq(Undeletable.PROP_DELETED, Boolean.FALSE);
            criteria.add(notDeleted);
        }
        criteria = appendDefaultOrder(criteria);
        return findByCriteria(criteria);
    }

    @Override
    public long countAll() {
        Criteria criteria = createSimpleCriteria();
        criteria.setProjection(Projections.count(Identifiable.PROP_ID));
        if (_deletable) {
            Criterion notDeleted = Restrictions.eq(Undeletable.PROP_DELETED, Boolean.FALSE);
            criteria.add(notDeleted);
        }
        Long count = (Long) criteria.uniqueResult();

        return count;
    }

	@SuppressWarnings("unchecked")
    @Override
    public PaginatedList<T> getPage(int pageIndex, int pageSize) {
        int totalItems = (int) countAll();
        if (totalItems <= 0) {
            return new PaginatedListImpl<>();
        }

        // get a partial list of items
        PaginatedList<T> paginatedList = new PaginatedListImpl<>(totalItems, pageIndex, pageSize);
        Criteria criteria = createSimpleCriteria();
        criteria = appendDefaultOrder(criteria);
        if (_deletable) {
            Criterion notDeleted = Restrictions.eq(Undeletable.PROP_DELETED, Boolean.FALSE);
            criteria.add(notDeleted);
        }
        criteria.setFirstResult(paginatedList.getPageIndex() * paginatedList.getPageSize());
        criteria.setMaxResults(paginatedList.getPageSize());

        List<T> pageList = criteria.list();
        paginatedList.setPageList(pageList);

        return paginatedList;
    }

    @SuppressWarnings("rawtypes")
    @Override
	public final List<T> findByExample(T example) {
        Criteria criteria = createSimpleCriteria();
        PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(_clazz);
        for (PropertyDescriptor descriptor : descriptors) {
            String propName = descriptor.getName();
            
            String[] props = {Identifiable.PROP_ID,Activable.PROP_ACTIVE,Defaultable.PROP_DEFAULT};
            
            if (ArrayUtils.contains(props, propName)) {
                continue;
            }
            
            if (propName.equals(Undeletable.PROP_DELETED)) {
                // always get item is not deleted
                criteria.add(Restrictions.eq(Undeletable.PROP_DELETED, Boolean.FALSE));
                continue;
            }
            
            Method method = descriptor.getReadMethod();
            if (method == null || (!method.isAnnotationPresent(Column.class) && !method.isAnnotationPresent(Basic.class))) {
                continue;
            }
            
            Object value = null;

            try {
                method.setAccessible(true);
                value = method.invoke(example);
            } catch (IllegalArgumentException e) {
                continue;
            } catch (IllegalAccessException e) {
                continue;
            } catch (InvocationTargetException e) {
                continue;
            }

            if (value == null) {
                continue;
            }
            
            //special case position of node  = 0 that is default value -> fix default page
            if (propName.equals("position") && value.equals(0)) {
                continue;
            }
            
            criteria.add(Restrictions.eq(propName, value));
        }

        criteria = appendDefaultOrder(criteria);
        if (example instanceof Inheritable) {
            if (((Inheritable) example).getParent() == null) {
                criteria.add(Restrictions.isNull(Inheritable.PROP_PARENT));
            } else {
                criteria.add(Restrictions.eq(Inheritable.PROP_PARENT, ((Inheritable) example).getParent()));
            }
        }

        return findByCriteria(criteria);
    }

    @SuppressWarnings(value = "unchecked")
    @Override
    public final void setAsDefault(Defaultable<I> object) {
        if (object.getExample() != null) {
            List<T> objects = findByExample((T) object.getExample());
            for (T o : objects) {
                if (object != o && o instanceof Defaultable) {
                    ((Defaultable<T>) o).setDefault(Boolean.FALSE);
                    _entityManager.merge(o);
                }
            }
        }
        object.setDefault(true);

        if (((T) object).getId() != null) {
            _entityManager.merge(object);
        } else {
            _entityManager.persist(object);
        }
    }

    @Override
    public void save(final T object) {
    	if (object.getId() != null) {
            T result = _entityManager.merge(object);
        	if(logger.isDebugEnabled()) {
        		logger.debug("Merging Object : " + result) ;
        	}
        } else {
            _entityManager.persist(object);
        	if(logger.isDebugEnabled()) {
        		logger.debug("Persisting Object : " + object) ;
        	}
        }
    }

    @SuppressWarnings("unchecked")
    @Override
   public void save(final T... objects) {
        for (T object : objects) {
            save(object);
        }
    }

    @Override
    public void delete(final T object, boolean checkIdDefault) throws UnsupportedOperationException {
        if (checkIdDefault) {
            if (_defaultable) {
                checkIfDefault(object);
            }
        }
        if (_deletable) {
            ((Undeletable) object).setDeleted(true);
            _entityManager.merge(object);
        } else {
            _entityManager.remove(object);
        }
    }

    @Override
    public final T refresh(T entity) {
    	T refreshed = entity;
    	if(entity != null) {
    		refreshed = _entityManager.merge(entity);
    	}
    	return refreshed;
    }

    @Override
    public final void flushAndClear() {
        _entityManager.flush();
        _entityManager.clear();
    }

    @SuppressWarnings("unchecked")
    @Override
	public final List<T> findByCriteria(Criteria criteria) {
        Criteria _criteria = appendDefaultOrder(criteria);
        return _criteria.list();
    }

    @SuppressWarnings("unchecked")
    @Override
    public final T findUniqueByCriteria(Criteria criteria) throws NonUniqueResultException, NoResultException {
        return (T)criteria.uniqueResult();
    }

    @Override
    public Order getDefaultOrder() {
    	if(ClazzUtils.isImplementInterface(getClazz(), Identifiable.class)){
    		return Order.asc(Identifiable.PROP_ID);
    	}
    	return null;
    }
    
    @Override
	public T loadLast() {
    	Criteria criteria = createSafeCriteria();
    	DetachedCriteria maxId = DetachedCriteria.forClass(getMappedClass());
    	maxId.setProjection(Projections.max(Identifiable.PROP_ID));
    	criteria.add(Property.forName(Identifiable.PROP_ID).eq(maxId));
		return findUniqueByCriteria(criteria);
	}

	/**
     * Get entity manager.
     *
     * @return entity manager
     */
    protected final EntityManager getEntityManager() {
        return _entityManager;
    }

    /**
     * special for Query using JpaTemplate
     *
     * @param query Query string
     * @param alias alias which is append before the order property
     * @return query string
     */
    protected String appendDefaultOrder(String query, String alias) {
        if (getDefaultOrder() != null) {
            return query + orderToSqlString(getDefaultOrder(),alias);
        }
        return query;
    }
    
    /**
     * Special for query using Criteria
     *
     * @return Criteria
     */
    protected Criteria appendDefaultOrder(Criteria criteria) {
        if (getDefaultOrder() != null) {
            return criteria.addOrder(getDefaultOrder());
        }

        return criteria;
    }
    
    @Override
	public void flush() {
		getEntityManager().flush();
	}

    @Override
	public Criteria createSimpleCriteria() {
		return  getSession().createCriteria(getEntityClass());
	}

    @Override
    public Criteria createSafeCriteria() {
		Criteria criteria = createSimpleCriteria();
		if(ClazzUtils.isImplementInterface(Undeletable.class, getEntityClass())) {
			criteria.add(Restrictions.eq(Undeletable.PROP_DELETED, Boolean.FALSE));
		}
		
		if(ClazzUtils.isImplementInterface(Activable.class, getEntityClass())) {
			criteria.add(Restrictions.eq(Activable.PROP_ACTIVE, Boolean.TRUE));
		}
		return criteria;
    }
    
    @Override
    public Query createQuery(String hqlQuery) {
    	return getSession().createQuery(hqlQuery);
    }

    private static String orderToSqlString(Order order, String alias) {
        return " ORDER BY " + (alias != null ? alias + "." : "") + order.toString(); 
    }
    
    private Session getSession() {
    	Session session = getEntityManager().unwrap(Session.class);
		if(!session.isOpen()) {
			session = session.getSessionFactory().openSession();
		}
		return session;
    }

	@Override
	public T find(Class<T> type, I primaryKey) {
		return getEntityManager().find(type, primaryKey);
	}
    
}
